查看原文
其他

Go:建议你使用专门的构造函数

程序员ug 幽鬼 2022-09-08

争做团队核心程序员,关注「幽鬼

大家好,我是程序员幽鬼。

Go 语言并非完全的面向对象语言,只是有部分面向对象特性。其中,没有实际意义的构造函数,但对类似构造函数有自己的一些约定成俗的规则。本文讲解为什么在 Go 中,建议你尽量使用专门的构造函数。

01 Go 和 Struct 初始化的背景

Go 有一个特殊的特性。它不同于其他语言,Go 的构造函数也是独一无二的。我们通过比较其他语言如何构造对象来建立基准。

PHP

<?php
class Point {
    protected int $x;
    protected int $y;

    public function __construct(int $x, int $y = 0) {
        $this->x = $x;
        $this->y = $y;
    }
}

$p1 = new Point(45);

Java 重载(多个构造函数)

public class MyClass {

    private int number = 0;

    public MyClass() {
    }

    public MyClass(int theNumber) {
        this.number = theNumber;
    }
}

Solidity

pragma solidity ^0.5.0;

contract Base {
uint data;

constructor(uint _data) public {
data = _data;
}
}

Go 呢?

Go 语言不强制构造函数(没有语法层面的构造函数)。它经常通过 “复合字面量” 来实例化结构体。

复合字面量为结构体、数组、切片和 map 构造值,并在每次计算(evaluated)它们时创建一个新值。

它们由字面值类型后跟花括号绑定的元素列表组成。相应的键可以可选地位于每个元素之前。

复合字面量示例:

type Point3D struct { x, y, z float64 }
type Line struct { p, q Point3D }

可以这么初始化:

origin := Point3D{}                            // zero value for Point3D
line := Line{origin, Point3D{y: -4, z: 12.3}}  // zero value for line.q.x

“作为一种限制情况,如果复合字面量根本不包含任何字段,它会为该类型创建一个零值。表达式new(Point3D)&Point3D{}是等效的。”

你可以将其记住为,如果未提供参数,则各字段使用其类型的默认值。Type + Curly Brackets.

Go 的设计考虑了简单性和无样板代码,复合字面量正是如此,但它们**不必要地难以维护。

为什么?让我用复合字面量解释我的日常问题以及我喜欢如何解决它们。

02 创建构造函数保持代码的可维护性

我的构造函数规则:

当创建一个新的结构体,比如 Transaction(Tx),我马上创建一个专用的构造器函数:NewTx()

type Tx struct {
 From     common.Address `json:"from"`
 To       common.Address `json:"to"`
 Gas      uint           `json:"gas"`
 GasPrice uint           `json:"gasPrice"`
 Value    uint           `json:"value"`
 Nonce    uint           `json:"nonce"`
 Data     string         `json:"data"`
 Time     uint64         `json:"time"`
}

func NewBaseTx(from, to common.Address, value uint, nonce uint, data string) Tx {
 return Tx{from, to, TxGas, TxGasPriceDefault, value, nonce, data, uint64(time.Now().Unix())}
}

这么做,我认为有 4 点原因,相信对你会有帮助。

原因 1 - 有些没有合适的默认值

没有构造函数

假设我没有创建专用的构造函数 NewBaseTx()。如果我必须向 Tx 结构添加另一个属性MaxGasPrice,我将不得不修改数十/数百个文件(取决于代码库大小/实现)。我这里没有夸大其词。这是一个非常现实的估计,因为我已经在各种项目中遇到过几次这样的情况。

too few constructor values

字段:值构造函数

但是,如果我将构造函数元素标记为“显式 字段:值 对,初始化程序可以按任何顺序出现,缺少的当作零值”,重构可能很快以生产错误告终,因为编译器不会指出我忘记传递新值的所有地方;所以这是更糟糕的方法

MaxGasPrice默认情况下会意外地为 0,从而使结构变为无效状态。甚至不要尝试使用可选的 setter 来修复它。

field:value zero

通常,我在field:value构造函数中看不到任何值,因为任何好的 IDE 都会像我之前的屏幕截图一样以图形方式向你显示名称。你有什么经验?同意还是您有不同的看法?

专用构造函数

幸运的是,我可以避免所有这些混乱,并为我的队友避免大量 PR,因为我可以控制结构的创建。我把这个职责封装成一个单一的功能。我只需要改变一个地方。

constructor values

每个结构都值得拥有它的构造函数!你未来的自己会在下一次重构会议上感谢你 :)

客观地说,有些结构体的默认值很特别,比如我经常使用的 sync.RWMutex

type Mutex struct {
 state int32
 sema  uint32
}

type RWMutex struct {
 w           Mutex  // held if there are pending writers
 writerSem   uint32 // semaphore for writers to wait for completing readers
 readerSem   uint32 // semaphore for readers to wait for completing writers
 readerCount int32  // number of pending readers
 readerWait  int32  // number of departing readers
}

rwm := sync.RWMutex{}
rwm.Lock()

十分优雅。但我写 Go 已经 4 年了,我无法确定我单独使用默认值设计的单个结构。但是,我可以在过去 6 个月内链接到 3 个 PR,这些 PR 由于缺少专用构造函数而导致生产错误。提供了错误的默认值,因为 Go 编译器无法挑到它;从编译器的角度来看,该值不会丢失;它认为你没有通过它,因为你希望编译器使用默认值。

原因 2 - 你的代码可能每周更改一次,而 RWMutex 不会

Mutex 源源的最后一次提交是在 2019 年 11 月。现在是 2021 年 11 月。

last commit mutex

不知道你是什么情况,但我项目中的最新提交是 3 小时前~。

如果你在项目中作为一个团队来优化灵活性和重构,那将是最好的。专用的构造函数可以帮助解决这个问题。

原因 3 - 导出的结构体和公共库

公开的结构体和库对变化更加敏感。上个月,由于缺少构造函数,我的代码出现了一个 bug。。。

一个Config结构体是用 field:value 语法 初始化的,我添加了一个不会有“合理默认值”的新字段:

Config{
 IsObserverEnabled: func() bool {
  return false
 },
 IsFilteringEnabled: func() bool {
  return false
 },
}

我审查了整个代码库,调整了所有用法,更新了测试,然后发布新版本。

似乎一起都很正常。但当我将库导入到 3 个使用它的微服务时,我忽略了一个用法,并且错误的默认值潜入并弄乱了服务部署。如果只有一个专用的NewConfig()构造函数,编译器会在导入库后抛出错误。我也不必更改 3 个微服务,只需更改NewConfig()库中的一个函数即可!

原因 4 - 打破单一职责原则 (SRP)

似乎是因为缺少样板,当开发人员创建专用构造函数时,他们会用不属于那里的额外逻辑来劫持它,比如 Goroutines, disk 操作, DB 链接等。

func NewDatabase(cfg Config) (*DB, error) {
 ctx, cancel := context.WithCancel(context.Background())

 db := &DB{
  // stuff
 }

 go db.connect(ctx)

 return db
}

污染的构造函数使得维护、测试和使用变得具有挑战性。

A constructor should only create a new instance of the object

就是这样。几十年来一直如此,这一原则对我们很有帮助。当我在某个库上调用 NewDatabase(cfg) 时,我最不希望看到的是在幕后的 goroutine 中启动数据库网络连接。

你无法对其进行测试,甚至无法在数据库连接之前初始化结构体;这只是一种可怕的做法。此逻辑属于专用Connect()函数,而不是构造函数。两种不同的职责。

以上就是我使用 Go 和构造函数的经验,希望对你有帮助!

原文链接:https://web3.coach/golang-why-you-should-use-constructors




往期推荐


欢迎关注「幽鬼」,像她一样做团队的核心。


您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存